﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace library
{
    static class Peers
    {
        internal static ManualResetEvent idlePeerEvent = new ManualResetEvent(true);

        internal static ManualResetEvent idleGetPeerEvent = new ManualResetEvent(true);

        static List<Peer> peers = new List<Peer>();

        #region Thread Refresh

        internal static void Start()
        {
            Load();

            Thread thread = new Thread(Refresh);

            thread.Start();
        }

        internal static void Stop()
        {
            idlePeerEvent.Set();

            idleGetPeerEvent.Set();

            Save();
        }

        static void Refresh()
        {
            while (!Client.Stop)
            {
                p2pRequest.Send(command: RequestCommand.Peer);

                // BurnYourDead();
            }
        }

        #endregion

        #region Queue

        static Queue<Peer> queue = new Queue<Peer>();

        #endregion

        static void Load()
        {
            if (!File.Exists(Parameters.peersPath))
                return;

            byte[] buffer = File.ReadAllBytes(Parameters.peersPath);

            int reg_size = Parameters.addressSize + 6; //ip:port

            Peer[] pp = new Peer[buffer.Length / reg_size];

            for (int i = 0; i < buffer.Length / reg_size; i++)
            {
                pp[i] = Peer.FromBytes(buffer.Skip(i * (reg_size)).Take(reg_size).ToArray());
            }

            lock (peers)
                peers.AddRange(pp);
        }

        static void Save()
        {
            lock (peers)
                File.WriteAllBytes(Parameters.peersPath, ToBytes(peers));
        }

        static void BurnYourDead()
        {
            lock (peers)
                peers = peers.Where(x => !x.HeIsDeadJim()).ToList();
        }

        internal static void AddPeer(Peer peer)
        {
            lock (peers)
            {
                if (!peers.Any(x => Addresses.Equals(peer.Address, x.Address)) && !peer.EndPoint.Equals(Client.LocalPeer.EndPoint))
                    peers.Add(peer);
            }

            idlePeerEvent.Set();

            idleGetPeerEvent.Set();
        }

        internal static void AddPeersFromBytes(IEnumerable<byte> request)
        {
            int reg_size = Parameters.addressSize + Parameters.ipv4Addresssize + sizeof(Int16);

            int c = request.Count() / reg_size;

            for (int i = 0; i < c; i++)
            {
                Peer peer = Peer.FromBytes(request.Skip(reg_size * i).Take(reg_size).ToArray());

                AddPeer(peer);
            }
        }

        internal static byte[] ToBytes(List<Peer> peerList)
        {
            int reg_size = Parameters.addressSize + Parameters.ipv4Addresssize + sizeof(Int16);

            byte[] result = new byte[peerList.Count() * reg_size];

            int i = 0;

            foreach (Peer p in peerList)
                p.ToBytes().Take(reg_size).ToArray().CopyTo(result, i++ * reg_size);

            return result;
        }

        internal static Peer GetPeerOrCreate(IPEndPoint remoteEndPoint)
        {
            Peer peer;

            lock (peers)
                peer = peers.FirstOrDefault(x => x.EndPoint.GetHashCode() == remoteEndPoint.GetHashCode());

            if (peer == null)
            {
                peer = new Peer(remoteEndPoint);

                lock (peers)
                    peers.Add(peer);
            }

            return peer;
        }

        internal static Peer[] GetPeers(
            byte[] closestToAddress = null,
            byte[] excludeOriginAddress = null,
            Peer[] excludeOriginPeer = null,
            RequestCommand command = RequestCommand.None,
            int count = 0,
            bool ignoreDeadEnd = true,
            bool wait = true)
        {
            var result = new Peer[0];

            switch(command)
            {
                case RequestCommand.Data:
                case RequestCommand.Metadata:

                    if (!Peers.idlePeerEvent.WaitOne(wait ? Timeout.Infinite : 0))
                        return result;

                    break;

                case RequestCommand.Peer:

                    if (!Peers.idleGetPeerEvent.WaitOne(wait ? Timeout.Infinite : 0))
                        return result;

                    break;
            }

            if (Client.Stop)
                return result;

            if (count == 0)
            {
                if (command == RequestCommand.None)
                {
                    lock (peers)
                        count = peers.Count();
                }
                else
                    count = Parameters.propagation;
            }



            lock (peers)
            {
                result = peers.

                    Where(x =>

                        (excludeOriginAddress == null
                            || !Addresses.Equals(x.Address, excludeOriginAddress))

                        && (excludeOriginPeer == null
                            || !excludeOriginPeer.Any(y => x.Equals(y)))

                        && (closestToAddress == null
                            || (!ignoreDeadEnd && Addresses.Compare(x.Address, closestToAddress) < Addresses.Compare(Client.LocalPeer.Address, closestToAddress))
                            || ignoreDeadEnd)

                        && !Client.LocalPeer.Equals(x)

                        && (command == RequestCommand.None
                            || ((command == RequestCommand.Data || command == RequestCommand.Metadata) && x.IsIdle)
                            || (command == RequestCommand.Peer && x.IsGetPeerIdle))).

                    OrderBy(x =>

                        closestToAddress == null ?

                        Utils.Rand.Next() : //todo: inverter probabilidade de pegar peer próximo ao seu endereço

                        Addresses.Compare(x.Address, closestToAddress)).

                    Take(count * 2).

                    OrderByDescending(x => x.Distance.Median).

                    Take(count).

                    ToArray();

                for (int i = 0; i < result.Length; i++)
                {
                    if (command == RequestCommand.Data || command == RequestCommand.Metadata)
                        result[i].CountRequisition();

                    else if (command == RequestCommand.Peer)
                        result[i].BeginGetPeer();
                }

                if (!peers.Any(x => !Client.LocalPeer.Equals(x) && x.IsGetPeerIdle))
                    idleGetPeerEvent.Reset();

                else if (!peers.Any(x => !Client.LocalPeer.Equals(x) && x.IsIdle))
                    idlePeerEvent.Reset();

                return result;
            }
        }

        internal static Peer GetPeer(
            byte[] closestToAddress = null,
            byte[] excludeOriginAddress = null,
            Peer[] excludeOriginPeer = null,
            RequestCommand command = RequestCommand.None,
            bool ignoreDeadEnd = true,
            bool wait = true)
        {
            return
                GetPeers(closestToAddress, excludeOriginAddress, excludeOriginPeer, command, 1, ignoreDeadEnd, wait)
                    .FirstOrDefault();
        }

        internal static bool Any()
        {
            return peers.Any();
        }
    }

    //command
    //    HttpGet = 542393671
    // case Command.HttpGet:
    //{
    //    string str = "HTTP/1.x 200 OK\r\n";
    //    str += "Content-Disposition: attachment; filename=windows.zip\r\n";
    //    str += "Content-Type: application/octet-stream\r\n";
    //    str += "Content-Length: 39450\r\n\r\n";

    //    byte[] bb = System.Text.Encoding.ASCII.GetBytes(str);

    //    state.socket.BeginSend(bb, 0, bb.Length, 0, new AsyncCallback(OnHttp), state);

    //    return;

    //}
}